"""扫描当前设备上可用的fpga，配置好串口号、序列号和fpga_socket的端口号，配置有许多方法"""
# import datetime
import json
import socket
import socketserver
import threading
from typing import Optional
import logging

import serial
import serial.tools.list_ports_windows as list_ports

import openocd
import utils
import os

CONFIG_PATH = 'fpgas.json'
EZ_PROTOCOL_HEAD_LENGTH = 6
IP_ADDRESS = utils.get_ip()
__fpga_cfg: dict
"""fpga的配置信息, 来源于配置文件"""

__initialized: bool = False

logger = logging.getLogger('logger')


class FPGAHandler(socketserver.BaseRequestHandler):
    """当client连接到fpga_socketserver时的处理函数"""

    def handle(self):
        # self.request is the TCP socket connected to the client
        # 多个用户尝试连接时，只允许最先到达的用户进入，其余用户阻塞在等候队列中
        # 用户使用socket连接时应设置超时选项
        self.request: socket.socket
        self.server: FPGAServer
        f: FPGA = self.server.fpga
        code = bytearray(EZ_PROTOCOL_HEAD_LENGTH)
        # 对状态进行简要判断
        if f.state != FPGA.SOCKET_READY:
            if f.state == FPGA.SOCKET_CONNECTED:
                logger.warning(f'fpga {f.usb_serial_num} is busy')
                self.request.close()
                return
            else:
                logger.warning(f'fpga {f.usb_serial_num}.state: {f.state} is not correct')
                self.request.close()
                return
        f.state = FPGA.SOCKET_CONNECTED
        logger.info(f'{self.client_address} is connected')
        parser = utils.ProtocolProcessor(bytes(f.tag, 'ascii'))
        # noinspection PyBroadException
        try:
            while True:
                length = self.request.recv_into(code, EZ_PROTOCOL_HEAD_LENGTH)
                # 如果对方断开连接，则length为空，结束本次连接
                if length == 0:
                    break
                if length < EZ_PROTOCOL_HEAD_LENGTH:
                    # 长度小于协议头，忽略本次指令
                    logger.warning(f'the length of the command head is wrong: {length}, {code[0:length]}')
                    continue
                op, para = parser.decode(code, self.request)
                logger.debug(op)
                if op == 'flush':
                    para: int
                    # msg_waitall指示阻塞直到读完para个字节
                    data = self.request.recv(para, socket.MSG_WAITALL)
                    logger.info(f'received bit file size: {len(data)}')
                    path = 'bit_file/' + f.usb_serial_num + '.bit'
                    fp = open(path, mode='wb')
                    fp.write(data)
                    fp.flush()
                    fp.close()
                    res = f.program(path)
                    os.remove(path)
                    if not res:
                        # 烧录失败，抛出异常
                        raise RuntimeError('program failed')
                    else:
                        # 烧录成功，发送progress报文
                        self.request.send(parser.encode_ez(parser.COMMAND_PROGRESS, b'\xFF\xFF\xFF\xFF'))
                elif op == 'display':
                    # 新开线程，该完成fpga -> node -> server的数据流传输
                    if f.state == FPGA.SAMPLING_MODE:
                        logger.warning(f'fpga {f.usb_serial_num} already in sampling_mode')
                        continue
                    f.state = FPGA.SAMPLING_MODE
                    # 开始采样
                    th = threading.Thread(target=f.rserial_tserver, daemon=True, args=[self.request, parser],
                                          name='sampling&' + f.usb_serial_num)
                    f.clear_serial()
                    f.serial.write(f.start_sampling_command)
                    th.start()
                    logger.info(f'thread {th.name} is working now')
                elif op == 'operate':
                    para: bytearray
                    # i = int.from_bytes(para, byteorder='big')
                    # i = int.to_bytes(i, length=1, byteorder='big')
                    # 对minisys和ego1板，操作数只有第一个byte有效
                    i = para[0:f.operand_length]
                    f.serial.write(i)
                elif op == 'close':
                    break
                elif op == 'test':
                    para: int
                    data = self.request.recv(para)
                    logger.info(f'received bit file size: {len(data)}')
        except ConnectionError:
            logger.exception(f'handle: socket connection raised exception with '
                             f'{self.client_address}, {f.usb_serial_num}')
        except serial.SerialException:
            logger.exception(f'handle: serial exception raised')
            # 尝试重连
            # f.reset()
        except Exception:
            logger.exception(f'handle: exception with {self.client_address}, {f.usb_serial_num}')
        finally:
            # 等于时是子线程引发了异常
            if self.server.fpga.state != FPGA.SOCKET_READY:
                logger.info(f'{self.client_address} is closed')
                self.request.shutdown(socket.SHUT_RDWR)
                self.request.close()
                self.server.fpga.state = FPGA.SOCKET_READY
                # 若不能烧录将会抛出异常，此时说明该FPGA出错
                f.program_idle_bit()


class FPGAServer(socketserver.TCPServer):
    """与普通TCPServer相比，增加FPGA属性"""

    def __init__(self, f, server_address, RequestHandlerClass, bind_and_activate=True):
        self.fpga = f
        socketserver.TCPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate)


class FPGA:
    """FPGA类，保存有每个FPGA对象都应该有的参数"""
    # 初始化
    INIT = 0
    # 配置好串口号和端口号
    FPGA_READY = 1
    # 连接到串口
    SERIAL_CONNECTED = 2
    # 设置好socket
    SOCKET_READY = 3
    # 被客户端socket连接
    SOCKET_CONNECTED = 4
    # 采样模式
    SAMPLING_MODE = 5

    def __init__(self, tag: str, name: str, serial_com: str, usb_serial_num: str, operand_length: int,
                 io: dict) -> None:
        self.fpga_socketserver: Optional[FPGAServer] = None
        self.serial: Optional[serial.Serial] = None
        self.tag: str = tag
        self.name: str = name
        self.serial_com: str = serial_com
        self.usb_serial_num: str = usb_serial_num
        self.fpga_socket_port: int = 0
        self.state = self.INIT
        self.baud_rate: int = 230400
        self.operand_length: int = operand_length
        self.io = io
        self.converter: utils.Converter = utils.Converter(io)
        """Converter用于将采样数据转换成显示数据"""
        self.serial_recv_buffer = 150 * 4 * self.converter.sample_data_bytes
        """根据经验设计的串口接收缓冲区大小"""
        self.serial_recv_bytes = \
            (self.converter.sample_data_bytes - (32 * self.baud_rate // 10000) % self.converter.sample_data_bytes) \
            + (32 * self.baud_rate // 10000)
        """根据经验设计的一次接收串口数据的大小，其值的含义可认为是32ms内以baud_rate可以接收到的数据量，并与sample_data_bytes对齐"""
        self.start_sampling_command = b'\xff'
        """IP核中开始采样指令"""
        self.stop_sampling_command = b'\x7f'
        """IP核中停止采样指令"""

    def __str__(self) -> str:
        return f'{self.tag}: {self.serial_com}, {self.usb_serial_num}, {self.state}, {self.fpga_socket_port}'

    def connect_serial_com(self, baud_rate: int = 230400) -> bool:
        """打开self串口"""
        if self.state != self.FPGA_READY:
            logger.error(f'connect_serial_com: {self.usb_serial_num} is not ready')
            return False
        try:
            self.serial = serial.Serial(port=self.serial_com, baudrate=baud_rate, timeout=2)
            self.baud_rate = baud_rate
            # 最好是采样数据的倍数
            self.serial.set_buffer_size(self.serial_recv_buffer)
            self.serial.flush()
            logger.info(f'{self.usb_serial_num}.{self.serial_com} open')
            self.state = self.SERIAL_CONNECTED
            return True
        except IOError:
            logger.exception(f'{self.usb_serial_num}.{self.serial_com} failed to open')
            return False

    def bind_socket(self) -> bool:
        """绑定self.fpga_socket"""
        if self.state != self.SERIAL_CONNECTED:
            logger.error(f'bind_socket: {self.usb_serial_num}.serial is not connected')
            return False
        try:
            self.fpga_socket_port = utils.get_port(IP_ADDRESS, self.usb_serial_num)
            self.fpga_socketserver = FPGAServer(self, (IP_ADDRESS, self.fpga_socket_port), FPGAHandler)
            ip, port = self.fpga_socketserver.server_address
            utils.ip_cfg[self.usb_serial_num] = {'ipv4': ip, 'port': port}
            utils.sem.release()
            logger.info(f'{self.usb_serial_num}.port {self.fpga_socket_port} bind')
            self.state = self.SOCKET_READY
            return True
        except (ConnectionError, IOError):
            logger.exception(f'{self.usb_serial_num}.port {self.fpga_socket_port} failed to bind')
            return False

    def run(self):
        """运行每个FPGA的server_socket，你应该新开线程/进程来执行这个函数"""
        if self.state != self.SOCKET_READY:
            logger.error(f'run: {self.usb_serial_num}.socket is not ready')
            return False
        self.fpga_socketserver.serve_forever()

    def reset(self):
        """重置fpga，更改fpga状态"""
        # 查询该FPGA是否存在
        devices = list_ports.comports()
        is_exist = False
        for device in devices:
            if device.serial_number is not None and device.serial_number[0:-1] == self.usb_serial_num:
                is_exist = True
        if not is_exist:
            logger.error(f'reset: {self.usb_serial_num} is not existing on this machine')
            fpgas = get_fpgas()
            target = None
            for f in fpgas:
                if f.usb_serial_num == self.usb_serial_num:
                    target = f
                    break
            if target is not None:
                fpgas.remove(target)
            return
        # 重置fpga
        if self.fpga_socketserver is not None:
            try:
                self.fpga_socketserver.server_close()
            except ConnectionError:
                logger.exception(f'reset: {self.usb_serial_num}.server close')
        if self.serial is not None:
            try:
                self.serial.close()
            except IOError:
                logger.exception(f'reset: {self.usb_serial_num}.serial close')
        self.state = FPGA.FPGA_READY
        self.connect_serial_com()
        self.clear_serial()
        self.bind_socket()
        self.run()
        logger.info(f'{self.usb_serial_num} reset successfully')

    def program(self, bit_file_path: str) -> bool:
        """烧录bit文件到self上的fpga"""
        cfg = get_fpga_cfg()
        cfg_path = cfg[self.tag]['openocd_cfg']
        # 获取指定的烧录器
        ocd = openocd.get_program_func(self.tag)
        if ocd is None:
            logger.error(f'program: unsupported fpga: {self.tag}')
            return False
        return ocd.program(cfg_path, self.usb_serial_num, bit_file_path)

    def rserial_tserver(self, conn: socket.socket, parser: utils.ProtocolProcessor):
        """
        receive from serial and transmit to server
        从串口中读取数据，缓存起来，满足trigger时向服务器发送数据，需要单开线程完成
        目前是读够大约740个字节就向外发送数据，一般来说总能读够这么多字节
        """
        self.converter.clear_prev_out()
        try:
            while True:
                if self.state != FPGA.SAMPLING_MODE:
                    logger.info('rserial_tserver: sampling done, thread exited')
                    break
                b = self.serial.read(self.serial_recv_bytes)
                data_size = len(b)
                if not b:
                    continue
                if data_size < self.serial_recv_bytes:
                    if data_size % self.converter.sample_data_bytes == 0:
                        logger.info(f'serial read size {data_size} < {self.serial_recv_bytes}')
                    else:
                        logger.warning(f'serial read size {data_size} < {self.serial_recv_bytes}, '
                                       f'and it % {self.converter.sample_data_bytes} != 0')
                    logger.debug(f'recv bytes: \n{b.hex(" ")}')
                    continue
                logger.debug(f'recv data: {b.hex(" ")}')
                self.converter.convert_to_display_data(bytearray(b))
                # conn.send(self.converter.curOut)
                # 如果得到的展示数据不同再发送，相同就不再发送
                if not self.converter.cur_equal_prev():
                    self.converter.copy_to_prev()
                    # 如果外部conn断开，send()也会触发异常从而终止本线程
                    command = (parser.COMMAND_DISPLAY[0] | parser.COMMAND_EXTEND[0]).to_bytes(1, 'big', signed=False)
                    operand = (len(self.converter.curOut) << 3).to_bytes(2, 'big', signed=False) + self.converter.curOut
                    display_protocol_data = parser.encode_ez(command, operand)
                    logger.debug(f'send code: {display_protocol_data.hex(" ")}')
                    conn.send(display_protocol_data)
        # 如果串口出错，则退出线程，并断开socket
        except serial.SerialException:
            logger.exception('rserial_tserver: serial exception, thread exited, socket closed')
            conn.shutdown(socket.SHUT_RDWR)
            conn.close()
            self.state = FPGA.SOCKET_READY
        except OSError:
            logger.warning('rserial_tserver: exception raised, thread exited')

    def clear_serial(self):
        """
        pyserial库在清空串口缓冲区时似乎有bug，但幸运的是我们实现fpga的ip核时制定了采样开关
        本方法清理可能存在的垃圾数据，使用本方法时请保证stop_sampling_command能够正确阻止fpga发送采样数据
        """
        # 小于此状态时一定没有连接到串口
        if self.state < self.SERIAL_CONNECTED:
            logger.error(f'clear_serial: {self.usb_serial_num}.serial is not connected')
            return
        self.serial.write(self.stop_sampling_command)
        while True:
            # 设置了超时选项，读不到数据超时后b为空
            b = self.serial.read(self.serial_recv_buffer)
            if not b:
                break
            logger.info(f'clear_serial: clear serial garbage {len(b)} bytes')

    def init(self):
        if self.connect_serial_com():
            # 这似乎是pyserial的一个bug，读取时可能会遇到之前产生的垃圾数据
            # flushInput()对此不起作用，需要提前读完已有的数据，使用下面的方法清除数据
            self.program_idle_bit()
            self.clear_serial()
            self.bind_socket()

    def program_idle_bit(self):
        if not self.program('bit_file/' + self.name + '_idle.bit'):
            raise RuntimeError('init: exception with program idle bit file')


__fpgas: list[FPGA] = []
"""本机的fpga列表, 一个全局变量"""


def __get_all_supported_fpgas():
    """获取当前机器上插入的所有支持的fpga的usb信息"""
    global __fpgas
    global __fpga_cfg
    devices = list_ports.comports()
    for device in devices:
        # 过滤掉没有序列号的
        if device.serial_number is None:
            continue
        # 过滤掉序列号长度小于2的
        if len(device.serial_number) < 2:
            continue
        fpga_tag = device.serial_number[0:2]
        # 过滤掉不在配置文件上的
        if fpga_tag not in __fpga_cfg.keys():
            continue
        # 过滤掉pid和vid不匹配的
        if device.pid == __fpga_cfg[fpga_tag]['usb_pid'] and device.vid == __fpga_cfg[fpga_tag]['usb_vid']:
            __fpgas.append(FPGA(fpga_tag, __fpga_cfg[fpga_tag]['name'], device.name, device.serial_number[0:-1],
                                __fpga_cfg[fpga_tag]['operand_length'], __fpga_cfg[fpga_tag]['io']))
    if len(__fpgas) == 0:
        logger.error(f'No supported device detected! Detected devices are as follows:\n'
                     f'{[(d.serial_number, d.pid, d.vid, d.name, d.description) for d in devices]}\n'
                     f'Please check USB info, especially USB serial number.')
        raise RuntimeError('No supported device detected!')
    for f in __fpgas:
        # f.fpga_socket_port = utils.get_socket_port()
        f.state = FPGA.FPGA_READY


def init():
    with open(CONFIG_PATH, 'r') as fp:
        global __fpga_cfg
        __fpga_cfg = json.load(fp)
    # print(__fpgas.keys())
    __get_all_supported_fpgas()

    global __initialized
    __initialized = True
    # global __fpgas
    # for f in __fpgas:
    #     print(f)


def get_fpgas() -> Optional[list[FPGA]]:
    if not __initialized:
        logger.error('device_detector is not initialized!')
        return None
    global __fpgas
    return __fpgas


def get_fpga_cfg() -> Optional[dict]:
    if not __initialized:
        logger.error('device_detector is not initialized!')
        return None
    global __fpga_cfg
    return __fpga_cfg
